﻿namespace GreenIsomer.Web.Controls.JQuery {
	using System;
	using System.Web.UI;
	using System.Web.UI.WebControls;
	using System.ComponentModel;

	/// <summary>
	/// Server control that implements the jQuery UI Resizable interaction for a panel.
	/// </summary>
	[ParseChildren(false), PersistChildren(true)]
	public class ResizablePanel : JQueryWebControl, IThemed {


		#region Constants: Interaction property defaults

		private const bool defaultAnimate = false;
		private const string defaultAnimateDurationString = "fast"; //TODO: Differs from interaction default "slow" (do we want to?)
		private const string defaultAnimateEasing = "swing";
		private const string defaultAspectRatioString = "false";
		private const bool defaultAutoHide = false;
		private const int defaultDistance = 1;
		private const int defaultDelay = 0;
		private const bool defaultGhost = false;
		private const string defaultGridString = "";
		private const int defaultMinHeight = 10;
		private const int defaultMinWidth = 10;

		#endregion


		#region Static Constants: Theme property defaults

		private const bool defaultUseTheme = true;
		private const PanelType defaultPanelType = PanelType.Content;
		private const bool defaultHighlight = false;
		private const bool defaultShowErrorState = false;
		private const Priority defaultPriority = Priority.None;

		#endregion


		#region Static Fields: Interaction property defaults

		//If changing this value, be sure to change the corresponding string constant to match
		private static readonly Duration defaultAnimateDuration = Duration.Fast; //TODO: Differs from interaction default "slow" (do we want to?)
		private static readonly AspectRatio defaultAspectRatio = AspectRatio.Unconstrained;
		private static readonly Grid defaultGrid = Grid.None;

		#endregion


		#region Constructors

		/// <summary>
		/// Initializes a new instance of the GreenIsomer.Web.Controls.JQuery.ResizablePanel class.
		/// </summary>
		public ResizablePanel() : base(HtmlTextWriterTag.Div) { }

		#endregion


		#region Properties: Interaction initialization options

		/// <summary>
		/// Animates to the final size after resizing.
		/// A CSS style must be specified for the class "ui-resizable-helper" in order to see the pending resize.
		/// This property maps directly to the "animate" option when the implemented jQuery UI interaction is initialized.
		/// </summary>
		[DefaultValue(defaultAnimate), Category("Behavior")]
		public virtual bool Animate {
			get { return ViewState.GetValue("Animate", defaultAnimate); }
			set { ViewState["Animate"] = value; }
		}

		/// <summary>
		/// Duration time for animating, in milliseconds. Other possible values: slow, normal, fast.
		/// This property is directly mapped to the "animateDuration" option when the jQuery UI interaction is initialized.
		/// </summary>
		[DefaultValue(typeof(Duration), defaultAnimateDurationString), Category("Behavior")]
		public virtual Duration AnimateDuration {
			get { return ViewState.GetValue("AnimateDuration", defaultAnimateDuration); }
			set { ViewState["AnimateDuration"] = value; }
		}

		//TODO: You know the drill... Improve Effects engine to be like themes...
		/// <summary>
		/// Easing effect for animating.
		/// This property is directly mapped to the "animateEasing" option when the jQuery UI interaction is initialized.
		/// </summary>
		[DefaultValue(defaultAnimateEasing), Category("Behavior")]
		public virtual string AnimateEasing {
			get { return ViewState.GetValue("AnimateEasing", defaultAnimateEasing); }
			set { ViewState["AnimateEasing"] = value; }
		}

		/// <summary>
		/// If set to true (Current), resizing is constrained by the original aspect ratio. Otherwise a custom aspect ratio can be specified, such as 9 / 16, or 0.5.
		/// This property is directly mapped to the "aspectRatio" option when the jQuery UI interaction is initialized.
		/// </summary>
		[DefaultValue(typeof(AspectRatio), defaultAspectRatioString), Category("Behavior")]
		public virtual AspectRatio AspectRatio {
			get { return ViewState.GetValue("AspectRatio", defaultAspectRatio); }
			set { ViewState["AspectRatio"] = value; }
		}

		/// <summary>
		/// If set to true, automatically hides the handles except when the mouse hovers over the element.
		/// This property maps directly to the "autoHide" option when the implemented jQuery UI interaction is initialized.
		/// </summary>
		[DefaultValue(defaultAutoHide), Category("Behavior")]
		public virtual bool AutoHide {
			get { return ViewState.GetValue("AutoHide", defaultAutoHide); }
			set { ViewState["AutoHide"] = value; }
		}

		/// <summary>
		/// Tolerance, in milliseconds, for when resizing should start. If specified, resizing will not start until after mouse is moved beyond duration. This can help prevent unintended resizing when clicking on an element.
		/// This property maps directly to the "delay" option when the implemented jQuery UI interaction is initialized.
		/// </summary>
		[DefaultValue(defaultDelay), Category("Behavior")]
		public virtual int Delay {
			get { return ViewState.GetValue("Delay", defaultDelay); }
			set { ViewState["Delay"] = value; }
		}

		/// <summary>
		/// Tolerance, in pixels, for when resizing should start. If specified, resizing will not start until after mouse is moved beyond distance. This can help prevent unintended resizing when clicking on an element.
		/// This property maps directly to the "distance" option when the implemented jQuery UI interaction is initialized.
		/// </summary>
		[DefaultValue(defaultDistance), Category("Behavior")]
		public virtual int Distance {
			get { return ViewState.GetValue("Distance", defaultDistance); }
			set { ViewState["Distance"] = value; }
		}

		/// <summary>
		/// If set to true, automatically hides the handles except when the mouse hovers over the element.
		/// This property maps directly to the "ghost" option when the implemented jQuery UI interaction is initialized.
		/// </summary>
		[DefaultValue(defaultGhost), Category("Appearance")]
		public virtual bool Ghost {
			get { return ViewState.GetValue("Ghost", defaultGhost); }
			set { ViewState["Ghost"] = value; }
		}
		
		/// <summary>
		/// This is the CSS class that will be added to a proxy element to outline the resize during the drag of the resize handle. Once the resize is complete, the original element is sized.
		/// This property maps directly to the "helper" option when the implemented jQuery UI interaction is initialized.
		/// </summary>
		[DefaultValue(""), Category("Appearance")]
		public virtual string Helper {
			get { return ViewState.GetValue("Helper", string.Empty); }
			set { ViewState["Helper"] = value; }
		}

		/// <summary>
		/// Snaps the resizing element to a grid, every x and y pixels.
		/// This property is directly mapped to the "grid" option when the jQuery UI interaction is initialized.
		/// </summary>
		[DefaultValue(typeof(Grid), defaultGridString), Category("Behavior")]
		public virtual Grid Grid {
			get { return ViewState.GetValue("Grid", defaultGrid); }
			set { ViewState["Grid"] = value; }
		}

		//TODO: Make sure Layout is the right category for these properties
		/// <summary>
		/// The maximum height to which the panel can be resized, in pixels.
		/// This property maps directly to the "maxHeight" option when the jQuery UI interaction is initialized.
		/// </summary>
		[DefaultValue(null), Category("Layout")]
		public virtual int? MaxHeight {
			get { return ViewState.GetNullableValue<int>("MaxHeight"); }
			set { ViewState.SetNullableValue("MaxHeight", value); }
		}

		/// <summary>
		/// The maximum width to which the panel can be resized, in pixels.
		/// This property maps directly to the "maxWidth" option when the jQuery UI interaction is initialized.
		/// </summary>
		[DefaultValue(null), Category("Layout")]
		public virtual int? MaxWidth {
			get { return ViewState.GetNullableValue<int>("MaxWidth"); }
			set { ViewState.SetNullableValue("MaxWidth", value); }
		}

		/// <summary>
		/// The minimum height to which the panel can be resized, in pixels.
		/// This property maps directly to the "minHeight" option when the jQuery UI interaction is initialized.
		/// </summary>
		[DefaultValue(defaultMinHeight), Category("Layout")]
		public virtual int MinHeight {
			get { return ViewState.GetValue("MinHeight", defaultMinHeight); }
			set { ViewState["MinHeight"] = value; }
		}

		/// <summary>
		/// The minimum width to which the panel can be resized, in pixels.
		/// This property maps directly to the "minWidth" option when the jQuery UI interaction is initialized.
		/// </summary>
		[DefaultValue(defaultMinWidth), Category("Layout")]
		public virtual int MinWidth {
			get { return ViewState.GetValue("MinWidth", defaultMinWidth); }
			set { ViewState["MinWidth"] = value; }
		}

		#endregion


		#region Properties: Theme

		/// <summary>
		/// Gets or sets whether or not the control should be rendered with jQuery UI theme classes.
		/// </summary>
		[DefaultValue(defaultUseTheme), Category("Appearance")]
		public virtual bool UseTheme {
			get { return ViewState.GetValue("UseTheme", defaultUseTheme); }
			set { ViewState["UseTheme"] = value; }
		}

		/// <summary>
		/// Gets or sets a value representing logical jQuery UI component containers defined by the theme framework.
		/// </summary>
		/// <exception cref="ArgumentException">Value not defined.</exception>
		/// <seealso cref="UseTheme"/>
		[DefaultValue(defaultPanelType), Category("Appearance")]
		public PanelType PanelType {
			get { return ViewState.GetValue("PanelType", defaultPanelType); }
			set {
				if(!Enum.IsDefined(typeof(PanelType), value)) throw new ArgumentException("Value not defined.", "value");

				ViewState["PanelType"] = value; 
			}
		}

		/// <summary>
		/// Gets or sets whether or not the control should be rendered with a highlighted interaction cue defined by the theme framework.
		/// </summary>
		/// <seealso cref="UseTheme"/>
		[DefaultValue(defaultHighlight), Category("Appearance")]
		public virtual bool Highlight {
			get { return ViewState.GetValue("Highlight", defaultHighlight); }
			set { ViewState["Highlight"] = value; }
		}

		/// <summary>
		/// Gets or sets whether or not the control should be rendered with an error interaction cue defined by the theme framework.
		/// </summary>
		/// <seealso cref="UseTheme"/>
		[DefaultValue(defaultShowErrorState), Category("Appearance")]
		public virtual bool ShowErrorState {
			get { return ViewState.GetValue("ShowErrorState", defaultShowErrorState); }
			set { ViewState["ShowErrorState"] = value; }
		}

		/// <summary>
		/// Gets or sets a value representing priority-based interaction cues defined by the theme framework.
		/// </summary>
		/// <exception cref="ArgumentException">Value not defined.</exception>
		/// <seealso cref="UseTheme"/>
		[DefaultValue(defaultPriority), Category("Appearance")]
		public virtual Priority Priority {
			get { return ViewState.GetValue("Priority", defaultPriority); }
			set {
				if(!Enum.IsDefined(typeof(Priority), value)) throw new ArgumentException("Value not defined.", "value");

				ViewState["Priority"] = value;
			}
		}

		#endregion


		#region Properties: Subcontrol

		/// <summary>
		/// Gets or sets the hidden field used to round-trip the Width property.
		/// </summary>
		protected virtual PostBackField WidthField { get; set; }

		/// <summary>
		/// Gets the rendered element ID of the hidden field used to round-trip the Width property.
		/// </summary>
		public virtual string WidthClientId {
			get { return WidthField.ClientID; }
		}

		/// <summary>
		/// Gets or sets the hidden field used to round-trip the Height property.
		/// </summary>
		protected virtual PostBackField HeightField { get; set; }

		/// <summary>
		/// Gets the rendered element ID of the hidden field used to round-trip the Height property.
		/// </summary>
		public virtual string HeightClientId {
			get { return HeightField.ClientID; }
		}

		#endregion


		#region Methods: Rendering hooks

		/// <summary>
		/// Allows a jQuery server control to reference scripts and stylesheets necessary to render their client markup and widgets.
		/// </summary>
		/// <param name="manager">The GreenIsomer.Web.Controls.JQuery.Manager used to handle style and script referencing in addition to unified generation of load and ready scripts.</param>
		protected override void Register(Manager manager) {

			manager.RegisterScriptFile(Settings.Default.CoreScriptFile);
			manager.RegisterScriptFile(Settings.Default.UiCoreScriptFile);
			manager.RegisterScriptFile(Settings.Default.UiResizableScriptFile);

			manager.RegisterStyleFile(Settings.Default.UiCoreStyleFile);
			manager.RegisterStyleFile(Settings.Default.UiResizableStyleFile);
			manager.RegisterThemeStyleFile();

			manager.RegisterControl(this);

			EnsureChildControls();
		}

		/// <summary>
		/// Called by the ASP.NET page framework to notify server controls that use composition-based implementation to create any child controls they contain in preparatin for posting back or rendering.
		/// This method is used primarily by control developers.
		/// </summary>
		protected override void CreateChildControls() {
			base.CreateChildControls();

			//TODO: The naming has to be resolved (maybe)

			string idBase = ID + ClientIDSeparator;

			WidthField = new PostBackField();
			WidthField.ID = idBase + "Width";
			WidthField.Value = string.Empty;
			WidthField.ValueChanged += new EventHandler<PostBackFieldEventArgs>(WidthField_ValueChanged);
			Controls.Add(WidthField);

			HeightField = new PostBackField();
			HeightField.ID = idBase + "Height";
			HeightField.Value = string.Empty;
			HeightField.ValueChanged += new EventHandler<PostBackFieldEventArgs>(HeightField_ValueChanged);
			Controls.Add(HeightField);
		}

		/// <summary>
		/// Sends server control content to a provided System.Web.UI.HtmlTextWriter object, which writes the content to be rendered to the client.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="writer">A System.Web.UI.HtmlTextWriter that represents the output stream to render HTML content on the client.</param>
		protected override void Render(HtmlTextWriter writer) {

			if(!MasterUseCssStyling) {
				writer.AddStyleAttribute(HtmlTextWriterStyle.Overflow, "hidden");
				writer.AddStyleAttribute(HtmlTextWriterStyle.Padding, "6px");
			}

			string originalCssClass = CssClass;
			CssClass = this.GetThemedCssClass();

			base.Render(writer);

			CssClass = originalCssClass;
		}

		/// <summary>
		/// Renders the load script required to generate the client-side jQuery widget.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="script">A GreenIsomer.Web.Controls.JQuery.ScriptBuilder that creates the output stream to render jQuery JavaScript on the client.</param>
		protected override void RenderLoadScript(ScriptBuilder script) {

			script.RenderSelector(ClientID);

			script.Append(".resizable(");
			script.RenderProperties(CreateProperties());
			script.Append(");");
		}

		/// <summary>
		/// Creates the properties object that will be rendered as the jQuery UI initialization options.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <returns>A GreenIsomer.Web.Controls.JQuery.Properties object that will become the jQuery UI initialization options.</returns>
		protected override PropertyCollection CreateProperties() {
			PropertyCollection properties = base.CreateProperties();

			bool animate = Animate;
			if(animate != defaultAnimate) properties.AddValue("animate", animate);

			Duration animateDuration = AnimateDuration;
			//TODO: Differs from interaction default "slow" (do we want to?)
			if(animateDuration != Duration.Slow) properties.AddValue("animateDuration", animateDuration);

			string animateEasing = AnimateEasing;
			if(animateEasing != defaultAnimateEasing) properties.AddValue("animateEasing", animateEasing);

			AspectRatio aspectRatio = AspectRatio;
			if(aspectRatio != defaultAspectRatio) properties.AddValue("aspectRatio", aspectRatio);

			bool autoHide = AutoHide;
			if(autoHide != defaultAutoHide) properties.AddValue("autoHide", autoHide);

			int delay = Delay;
			if(delay != defaultDelay) properties.AddValue("delay", delay);

			int distance = Distance;
			if(distance != defaultDistance) properties.AddValue("distance", distance);

			bool ghost = Ghost;
			if(ghost != defaultGhost) properties.AddValue("ghost", ghost);

			Grid grid = Grid;
			if(grid != defaultGrid) properties.AddValue("grid", grid);

			string helper = Helper;
			if(!string.IsNullOrEmpty(helper)) properties.AddValue("helper", helper);

			int? maxHeight = MaxHeight;
			if(maxHeight.HasValue) properties.AddValue("maxHeight", maxHeight.Value);

			int? maxWidth = MaxWidth;
			if(maxWidth.HasValue) properties.AddValue("maxWidth", maxWidth.Value);

			int minHeight = MinHeight;
			if(minHeight != defaultMinHeight) properties.AddValue("minHeight", minHeight);

			int minWidth = MinWidth;
			if(minWidth != defaultMinWidth) properties.AddValue("minWidth", minWidth);

			ScriptBuilder stopScript = new ScriptBuilder();
			RenderStopScript(stopScript);
			if(stopScript.Length != 0) properties.AddValue("stop", stopScript);

			return properties;
		}

		/// <summary>
		/// Renders the client "stop" event script.
		/// This method is used primarily by control developers.
		/// </summary>
		/// <param name="script">A GreenIsomer.Web.Controls.JQuery.ScriptBuilder that creates the output stream to render jQuery JavaScript on the client.</param>
		public virtual void RenderStopScript(ScriptBuilder script) {

			bool animate = Animate;

			script.Append("function(event,ui){ ");

			script.NewLine();

			script.RenderSelector(WidthClientId);
			if(animate)   //TODO: There's bound to be a better way, but I don't have it
				script.Append(".val($(ui.helper).width() + $(ui.element).width() - $(ui.element).outerWidth()); ");
			else
				script.Append(".val(ui.size.width); ");

			script.NewLine();

			script.RenderSelector(HeightClientId);
			if(animate)   //TODO: There's bound to be a better way, but I don't have it
				script.Append(".val($(ui.helper).height() + $(ui.element).height() - $(ui.element).outerHeight()); ");
			else
				script.Append(".val(ui.size.height); ");

			script.NewLine();

			script.Append("}");
		}

		#endregion


		#region Event handlers

		//TODO: Decide if it's worth having an event exposed and do it...

		private void WidthField_ValueChanged(object sender, PostBackFieldEventArgs e) {

			if(string.IsNullOrEmpty(e.NewValue)) return;

			int width;
			if(!int.TryParse(e.NewValue, out width)) return;

			Width = new Unit(width, UnitType.Pixel);
		}

		private void HeightField_ValueChanged(object sender, PostBackFieldEventArgs e) {

			if(string.IsNullOrEmpty(e.NewValue)) return;

			int height;
			if(!int.TryParse(e.NewValue, out height)) return;

			Height = new Unit(height, UnitType.Pixel);
		}

		#endregion


	}

}